#!/usr/bin/python

"""   
    pyrats
    
    Pyrats scans and analyizes your PHP source code for potential vulnerabilities. 
    
    Pyrats a command line program, that should help get PHP developers a quick survey of common commands that 
    capitalize potential security vulnerabilities. 
    All files of a specified directory will be recursively scanned and evaluated. 
    The search process can be refined through different options.
        
    Copyright (C) 2008 Bernd Essl <bernd_at_b23.at>
    http://www.b23.at
    New BSD License

"""

__author__ = "Bernd Essl <bernd_at_b23.at>"
__version__ = "$Rev$"
__date__ = "$Date$"
__copyright__ = "Copyright (c) 2008 Bernd Essl"
__license__ = "New BSD License"

from optparse import OptionParser
import os, re


class FunctionSearch(object):
    """ class for searching patterns for PHP-functions in given text """

    def __init__(self, text, pattern):
        self.text = text
        self.pattern = pattern 
    
    def search(self, search_for=None):
        """ search for one pattern or for all patterns in text
        return list with pattern (key), line_number and codeline """
        l = []
        line_number = 0
        for line in self.text.splitlines():
            line_number += 1
            # search only for one pattern
            if search_for in self.pattern.keys():
                if self.pattern[search_for][1].search(line):
                    l.append([search_for, line_number, line])
            # search thru all patterns            
            else:
                for key in self.pattern.keys():
                    if self.pattern[key][1].search(line):
                        l.append([key, line_number, line])
        return l


class FileOperator(object):
    """ for file handling """
    
    def __init__(self,path="./"):
        self.path = path
    
    def get_files_in_cwd(self, php_only=None):
        """ return a list with all files (full path to file) in cwd (recrusive) """
        l = []
        for root, dirs, files in os.walk(self.path):
            # any files in folder?
            if files:
                for f in files:
                    # no double slashes in basedir path
                    if root == self.path:
                        full_path = root+f
                    else:
                        full_path = root+"/"+f
                    # search only .php files || all files
                    if php_only and f[-4:] == ".php" or php_only == None:
                        l.append(full_path)
        return l
    
    def get_filetext(self, file_name):
        """ return text from file """
        try:
            text = file(file_name).read()
        except IOError:
            return ""
        # if file is binary don't return it
        if "\x00" in text:
            return ""
        else:
            return text
        

def main():

    pattern = {
        'include' : ["include", re.compile(r'(include|require)(_once)?(\s+)?(\(.+\)|\'.+\'|".+")')],
        'system' : ["system", re.compile(r'(system|passthru|exec|shell_exec|proc_open|popen)(\s+)?\(.+\)')],
        'mail' : ["mail", re.compile(r'mail(\s+)?\(.+\)')],
        'mysql' : ["mysql", re.compile(r'mysql_query(\s+)?\(.+\)')],
        'header' : ["header", re.compile(r'header(\s+)?\(.+\)')],
        'comment' : ["comment", re.compile(r'(hack|todo|trick|kludge|steal|stolen|divert|evil|broke|fix|bypas)', re.IGNORECASE)],
        'javascript' : ["javascript", re.compile(r'(document\.write|eval(\s+)?\(|document\.cookie|window\.location|document\.URL)', re.IGNORECASE)],        
    }
    
    parser = OptionParser(usage="usage %prog [options] DIRECTORY",  version="%prog " + str(__version__))
    parser.add_option("-n","--no-statistic", dest="no_stats",  action="store_true", help="don't show statistics in the end")
    parser.add_option("-o", "--output", dest="output", help="format: 'xml' or 'csv'")
    parser.add_option("-s","--search", dest="search_for", help="search: %s" % pattern.keys())
    parser.add_option("-p","--php", dest="php_only",  action="store_true", help="search only in .php files")
    options, args = parser.parse_args()     

    path = "./"
    # if there is an argument, change the default search directory (./)
    if len(args): 
        if os.path.exists(args[0]):
            path = args[0]
    
    fo = FileOperator(path)
    # list with all files
    file_list = fo.get_files_in_cwd(php_only=options.php_only,)
    warnings_num = 0
    lines_num = 0
    warning_list = []
    # loop over all files in file_list
    for file in file_list:
        if options.no_stats == None and options.output == None:
            print "Analyzing " + file
        # get text of file and search for PHP-functions
        text = fo.get_filetext(file)
        if options.no_stats == None:
            lines_num += len(text.splitlines())
        c = FunctionSearch(text, pattern)
        hit_list = c.search(search_for=options.search_for)
        if len(hit_list):
            for hit in hit_list:
                warnings_num += 1
                warning_list.append([file, hit[0], hit[1], hit[2]])

    if options.output == "csv":
        # send seperated by semicolon values, text-delimiter doubles quotes to stdout
        print """"file name";"pattern";"line number";"code line";"""
        for w in warning_list:
            print """"%s";"%s";"%s";"%s";""" % (w[0], w[1], w[2], w[3].strip())
    elif options.output == "xml":
        # send xml format of result to stdout
        print """<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"""
        print "<found>"
        for w in warning_list:
            print """\t<file_name>%s</file_name>\n\t<pattern>%s</pattern>\n\t<line_number>%s</line_number>\n\t<code_line>%s</code_line>\n""" % (w[0], w[1], w[2], w[3].strip())
        print "</found>"
    else:
        # print result to stdout
        print
        for w in warning_list:
            print """@ %s: %s: \n%s: %s\n""" % (w[0], w[1], w[2], w[3])
    
    # show some analyzing statistic at the end
    if options.no_stats == None and options.output == None:
        print "-"*22
        print "Total files analyzed: %i\nTotal lines analyzed: %i\nTotal warnings: %i\n" % (len(file_list), lines_num, warnings_num)

        
if __name__ == "__main__":
    main()
